﻿using System;
using System.Data.Common;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using log4net;

namespace MyLib {
	/// <summary>
	/// 
	/// </summary>
	internal enum SessionFactoryTransactionStatus {
		/// <summary>
		/// 
		/// </summary>
		None = 0,

		/// <summary>
		/// 
		/// </summary>
		Executing = 1,

		/// <summary>
		/// 
		/// </summary>
		Complete = 2
	}

	/// <summary>
	/// Session factory.
	/// </summary>
	public abstract class SessionFactory {
		/// <summary>
		/// Internal log.
		/// </summary>
		private ILog m_logger = LogManager.GetLogger(typeof(SessionFactory));

		/// <summary>
		/// 
		/// </summary>
		private static readonly Regex PLACEHOLDER_REGEX = new Regex(@"\$\b*(\w+)\b*\$", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase);

		/// <summary>
		/// 
		/// </summary>
		private static readonly Regex SAFE_PLACEHOLDER_REGEX = new Regex(@"[\w\b_]+", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase);

		/// <summary>
		/// 
		/// </summary>
		private static readonly Regex PARAMETER_REGEX = new Regex(@"#\b*(\w+)\b*#", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase);

		private static SessionFactory m_defaultInstance;

		private string m_connectionString;

		private DbProviderFactory m_factory;

		private bool m_useParameterPrefixInSql;

		private string m_parameterPrefix;

		[ThreadStatic]
		private static ITransaction m_transaction;

		/// <summary>
		/// 
		/// </summary>
		/// <param name="factory"></param>
		/// <param name="connectionString"></param>
		/// <param name="parameterPrefix"></param>
		/// <param name="useParameterPrefixInSql"></param>
		protected SessionFactory(
			System.Data.Common.DbProviderFactory factory,
			string connectionString,
			string parameterPrefix,
			bool useParameterPrefixInSql) {
			System.Diagnostics.Debug.Assert(factory != null);
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(connectionString));

			m_factory = factory;
			m_connectionString = connectionString;
			m_parameterPrefix = parameterPrefix;
			m_useParameterPrefixInSql = useParameterPrefixInSql;
		}

		/// <summary>
		/// Default instance of session factory.
		/// </summary>
		public static SessionFactory DefaultInstance {
			get {
				if (m_defaultInstance == null)
					throw new InvalidOperationException("Default session factory has not been initialized.");

				return m_defaultInstance;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="connectionName"></param>
		public static void InitializeDefaultInstance(string connectionName) {
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(connectionName));

			var connectionSettings = System.Configuration.ConfigurationManager.ConnectionStrings[connectionName];
			var factory = DbProviderFactories.GetFactory(connectionSettings.ProviderName);
			var connectionString = connectionSettings.ConnectionString;

			if (connectionSettings.ProviderName == "MySql.Data.MySqlClient") {
				m_defaultInstance = new MySqlSessionFactory(factory, connectionString);
			} else {
				throw new InvalidOperationException(
					string.Format(@"Unknown provider name: ""{0}"".", connectionSettings.ProviderName));
			}
		}

		/// <summary>
		/// 
		/// </summary>
		protected DbProviderFactory Factory {
			get {
				return m_factory;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		protected String ParameterPrefix {
			get {
				return m_parameterPrefix;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		protected bool UseParameterPrefixInSql {
			get {
				return m_useParameterPrefixInSql;
			}
		}

		/// <summary>
		/// Create a new instance of DbCommand.
		/// </summary>
		/// <param name="sql"></param>
		/// <param name="commandType"></param>
		/// <param name="parameters"></param>
		/// <returns></returns>
		internal DbCommand CreateCommand(string sql, System.Data.CommandType commandType, params DbParameter[] parameters) {
			if (string.IsNullOrWhiteSpace(sql))
				throw new ArgumentNullException("sql");

			var command = m_factory.CreateCommand();
			command.CommandType = commandType;
			command.CommandText = CreateSqlText(sql, parameters);

			CreateParameters(command, parameters);

			WriteLog(command);

			return command;
		} // end of CreateCommand.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="command"></param>
		/// <param name="parameters"></param>
		private void CreateParameters(DbCommand command, params DbParameter[] parameters) {
			System.Diagnostics.Debug.Assert(command != null);

			if (parameters == null)
				return;

			foreach (var parameter in parameters) {
				command.Parameters.Add(
					CreateParameter(parameter));
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="parameter"></param>
		/// <returns></returns>
		protected abstract System.Data.IDbDataParameter CreateParameter(MyLib.DbParameter parameter);

		/// <summary>
		/// 
		/// </summary>
		/// <param name="parameters"></param>
		/// <param name="sql"></param>
		private string CreateSqlText(string sql, params DbParameter[] parameters) {
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql));
			System.Diagnostics.Debug.Assert(parameters != null);

			// Replace placeholder in sql text.
			var ret = PLACEHOLDER_REGEX.Replace(sql, _1 => {
				var pn = _1.Groups[1].Value;

				var parameter = parameters.FirstOrDefault(_2 => _2.Name == pn);
				if (parameter != null) {
					var ppv = Convert.ToString(parameter.Value);

					if (string.IsNullOrEmpty(ppv))
						return string.Empty;

					if (!SAFE_PLACEHOLDER_REGEX.IsMatch(ppv)) {
						m_logger.WarnFormat(@"Unsafe placeholder ""{0}""({1}) in ""{2}"".", pn, ppv, sql);
					}

					return ppv;
				} else {
					return string.Empty;
				}
			});

			// Replace parameters in sql text.
			if (m_useParameterPrefixInSql) {
				ret = PARAMETER_REGEX.Replace(ret, _1 => m_parameterPrefix + _1.Groups[1].Value).Trim();
			} else {
				ret = PARAMETER_REGEX.Replace(ret, _1 => _1.Groups[1].Value).Trim();
			}

			return ret.Trim();
		} // end of CreateSqlText.

		/// <summary>
		/// 
		/// </summary>
		public void BeginTransaction() {
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(m_connectionString));

			if (m_transaction != null && !m_transaction.Closed) {
				return;
			}

			var connection = m_factory.CreateConnection();
			connection.ConnectionString = m_connectionString;
			connection.Open();

			var transaction = connection.BeginTransaction();

			m_transaction = new AutoTransaction(connection, transaction);

			if (m_logger.IsDebugEnabled) {
				m_logger.DebugFormat("Begin scoped transaction.");
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public void Complete() {
			if (m_transaction != null && !m_transaction.Closed) {
				m_transaction.Commit();

				if (m_logger.IsDebugEnabled) {
					m_logger.DebugFormat("Commit scoped transaction.");
				}
			}

			m_transaction = null;
		}

		/// <summary>
		/// 
		/// </summary>
		public void EndTransaction() {
			if (m_transaction != null && !m_transaction.Closed) {
				m_transaction.Rollback();

				if (m_logger.IsDebugEnabled) {
					m_logger.DebugFormat("Rollback scoped transaction.");
				}
			}

			m_transaction = null;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public Session CreateSession() {
			if (m_transaction != null && !m_transaction.Closed) {
				return CreateSessionFromTransaction(false);
			} else {
				return CreateSession(false);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public Session CreateReadOnlySession() {
			if (m_transaction != null && !m_transaction.Closed) {
				return CreateSessionFromTransaction(true);
			} else {
				return CreateSession(true);
			}
		}

		/// <summary>
		/// Create a new session.
		/// </summary>
		/// <param name="isReadOnly"></param>
		/// <returns></returns>
		private Session CreateSession(bool isReadOnly) {
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(m_connectionString));

			var connection = m_factory.CreateConnection();
			connection.ConnectionString = m_connectionString;
			connection.Open();

			var transaction = connection.BeginTransaction();

			return new Session(this, connection, transaction, isReadOnly, true);
		}

		/// <summary>
		/// Create a new session from transaction.
		/// </summary>
		/// <param name="isReadOnly"></param>
		/// <returns></returns>
		private Session CreateSessionFromTransaction(bool isReadOnly) {
			System.Diagnostics.Debug.Assert(m_transaction != null);
			System.Diagnostics.Debug.Assert(!m_transaction.Closed);

			return new Session(this, m_transaction.Connection, m_transaction.Transaction, isReadOnly, false);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="command"></param>
		private void WriteLog(DbCommand command) {
			if (m_logger.IsDebugEnabled) {
				var log1 = new StringBuilder();
				log1.AppendLine();
				log1.AppendFormat("Type: {0}", command.CommandType);
				log1.AppendLine();
				log1.AppendFormat("Text: {0}", command.CommandText);
				if (command.Parameters.Count > 0) {
					log1.AppendLine();
				}
				for (int i = 0; i < command.Parameters.Count; ++i) {
					var cp = command.Parameters[i];

					if (cp.Value == null || cp.Value == DBNull.Value) {
						log1.AppendFormat("Parameter: {0}, {1}({2}) = NULL", cp.ParameterName, cp.DbType, cp.Size);
					} else if (cp.Size < 0) {
						log1.AppendFormat("Parameter: {0}, {1} = '{2}'", cp.ParameterName, cp.DbType, cp.Value);
					} else {
						log1.AppendFormat("Parameter: {0}, {1}({2}) = '{3}'", cp.ParameterName, cp.DbType, cp.Size, cp.Value);
					}

					if (i < command.Parameters.Count - 1) {
						log1.AppendLine();
					}
				}
				m_logger.DebugFormat("Execute sql: {0}", log1);
			}
		}
	} // end of SessionFactory.
}
